上一篇,我們用最快的方式,透過shader製作了球體。基本上這個球體就跟MeshBasicMaterial({color: 0x4c99ff})
一樣,其球體範圍內的像素全是藍色。
本篇將介紹Shader的概念。我們將透過快速修改Shader,先做出一個成果,然後解釋其原理。
我將介紹以下內容:
基本上是這樣的:three.js的程式碼事實上也是在shader運行,shader是各式各樣3D渲染函式庫(使用WebGLRenderer的函式庫)的底層,它渲染所有像素。
所以說:當你在使用three.js、P5.js、babylon.js或WebGL API時,你就正在用Shader,只是它在底層。
如果你還有印象的話,你應該會記得在「Day8: Three.js 你有被光速踢過嗎?解析3D界的黃猿——光的底層原理與介紹」討論到:three.js有光、有球體等物件。光照射到球體,出現亮面跟暗面。
為什麼可以產生亮面跟暗面?這是因為,three.js也正在用shader,每當它執行一個像素,就會計算該像素其光的強度。
光的強度 = 法線的單位向量.向光的單位向量
= |法線|*|向光|*cos(θ)
一旦像素計算結果趨近1,則該像素最亮,這是因為亮面的向光向量跟像素所處的球面法線向量相近;相反的,當像素計算結果趨近0,代表兩向量角度接近直角。
也就是說,three.js之所以可以幫每一個像素創造亮面跟暗面,那是因為它能夠在shader運算上述的邏輯。都是shader的功勞。
我們看完了three.js的底層是用shader創造而成,那我們其實也可以實作光源。我們將在下一篇創造環境光,但在這之前,我們必須讀懂shader所使用的程式語言GLSL。
GLSL其實跟C語言很相近,如果你過去熟悉C語言,那你應該很快就能上手。然而如果沒有接觸C語言就來涉獵Shader的話,你可能會需要知道如何理解GLSL,本篇介紹GLSL中所需理解的先備知識。
GLSL其實跟C語言很相近,但如果沒有接觸過C語言就跑來前端的話,那可能需要惡補一下,以下介紹GLSL:
GLSL的變數相當多種,舉凡int
, float
, bool
, vec2
, vec3
, vec4
等,一一介紹:
int
, float
, bool
:
前三個你應該可以猜的出來,就是整數、浮點數、布林值
// 請務必加上分號
int color = 1;
float brighness = 0.0;
bool isWhite = true;
vec2
, vec3
, vec4
:
它們即是Vector的縮寫,它可以一個數組,可以是二維、三維、四維,依照你的需求而定。
vec2 position = vec2(1.0, -1.0);
vec3 color = vec3(1.0, 0.0, 0.5);
vec4 colorWithAlpha = vec4(1.0, 0.0, 0.5, 0.5);
vec2
可以裝兩個數值,vec3
可以裝三個數值,以此類推
如果想像成物件存取這些可能比較好理解。
// 類似js的
const color = {r: 0, g:0, b:0}
跟js的差別在於:
數組的型別不限制僅為浮點數,整數、布林值都可以。但強烈建議都用浮點數。
vec3 redColor = vec3(1, 0, 0); // 給定整數合法,將得出 vec3(1.0,0.0,0.0)
vec3 yellowColor = vec3(1, true, false); // 給定布林合法,將得出 vec3(1.0,1.0,0.0)
vec3 whiteColor = vec3(1, true, 1.0); // 多重型別合法,將得出 vec3(1.0,1.0,1.0)
存取數組中的值,有多種別名。存取時,我們可以分別用r,g,b來依序取得數值,也可以x,y,z依序取得數值。
vec4 redColor = vec4(0.8, 0.3, 0.2, 1.0);
// 以下都能取得數組第一個值
float red = redColor.r // 0.8
float x = redColor.x // 0.8
// 以下都能取得數組第二個值
float red = redColor.g // 0.3
float x = redColor.y // 0.3
// 以下都能取得數組第三個值
float red = redColor.b // 0.2
float x = redColor.z // 0.2
// 以下都能取得數組第四個值
float red = redColor.a // 1.0
float x = redColor.w // 1.0
取得多重數值的方式。GLSL中,可以取出多個數值,其解構的方式十分便利。
vec3 redColor = vec3(0.8, 0.3, 0.2);
// 可以指定成
vec4 noColor = vec3(redColor ,1.0) // 等於vec3(0.8, 0.3, 0.2. 1.0);
// 也可寫作
vec4 noColor = vec3(redColor.xyz ,1.0) // 等於vec3(0.8, 0.3, 0.2. 1.0);
// 可以抽特定的數值
vec4 noColor = vec3(redColor.xy ,0.7 ,1.0) // 等於vec3(0.8, 0.3, 0.7. 1.0);
// 抽取順序可以調整
vec4 noColor = vec3(redColor.yz ,0.7 ,1.0) // 等於vec3(0.3, 0.8, 0.7. 1.0);
函式跟JS的函式很像,但還是有差異,估計你可以直接透過用看的就能看出兩者差異:
// 回傳型別、函式名稱、參數
vec3 rgb(float r, float g, float b){
return vec3(r / 255.0, g / 255.0, b / 255.0);
}
我們有兩個Shader,vertextShader每個錨點執行一次,fragmentShader每一個像素執行一次。
凡是執行,就得有程式進入點。因為這不是逐行執行的腳本語言,這是程式語言,要編譯的那種。
而這也是為什麼,我們的fragmentShader跟vertextShader有main()
,因為它就是程式開始執行的地方。
// vertex shader
void main(void){
gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}
// fragment shader
void main(void){
gl_FragColor=vec4(0.0, 0.0, 0.0, 1.);
}
Shader有它的任務。vertex shader需要取得渲染範圍,而fragment shader需要每顆像素的顏色。
對vertex shader來說,只要得到gl_Position
數值(錨點最終在螢幕上的位置),就算完成任務。對fragment shader來說,只要得到gl_FragColor
(像素的顏色),就算完成任務。任務結束之後,剩下的邏輯都沒有必要了。
以vertex shader來說:
// vertex shader
// gl_Position代表螢幕像素的位置
gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
gl_Position
到底是什麼意義?
由於vertext shader每一個錨點執行一次,所以它可以取得球體的所有頂點,這也使得它能找出球體的形狀,在我們的電腦螢幕中,裁剪出一個範圍。
而這個範圍,使得fragment shader在運算每一個像素的顏色時,能夠得知有哪些像素需要運算,哪些不用運算。
以我們的例子來說,球體錨點中的範圍需要運算,球體以外的不需要運算。
projectionMatrix
、modelViewMatrix
是什麼意思?
主要是負責將錨點從世界空間中,基於鏡頭的位置,投影到螢幕這平面的座標中。這個之後可以介紹。
以fragment shader來說:
// fragment shader
// gl_FragColor代表每一個像素的顏色
gl_FragColor=vec4(0.0, 0.0, 0.0, 1.0); // 黑色
gl_FragColor就是每一個像素的顏色。
如果有100個像素,不就要設定100個gl_FragColor
嗎?這倒不用。設定1個就可以了,因為fragment shader每一顆像素都會各自執行fragment shader,每一顆像素都只要有一組RGBA顏色,所以一個gl_FragColor
就可以了。
我們快速修改Shader,並且介紹GLSL。然而還沒完呢!下一篇將繼續介紹GLSL,同時提供小東西實作。